﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TimbreRecognition.Recognition.Model.Kohenen;

namespace TimbreRecognition.Recognition.Teacher.Kohenen
{
    public class BatchKohenenTeacher : AbstractKohenenTeacher {

        private readonly static int T = 200;

        private ILogger logger;

        private int iterationNumber;

        private int s0;

        public override void teach(KohenenNetwork network, List<double[]> data)
        {

            s0 = (int) Math.Floor((double)Math.Min(network.getXDimension(), network.getYDimension()) / 2 );

            iterationNumber = 0;

            int inputCount = data[0].Length;
            double averageWeighError;

            do {

                network.resetWinCount();

                double totalError = 0;

                Dictionary<int, KohenenNeuron> winners = new Dictionary<int, KohenenNeuron>();

                for (int i = 0; i < data.Count; i++) {

                    double[] input = data[i];
                    network.recalculateOutput(input);

                    KohenenNeuron winner = network.getWinner();
                    winner.incWinCount();
                    winners.Add(i, winner);

                    totalError += getError(network, input);

                }

                changeWeight(network, winners, data);

                iterationNumber++;

                averageWeighError = totalError / data.Count / inputCount;

                Log("Iteration = " + iterationNumber + ", average weight error = " + averageWeighError);
            } while (iterationNumber < T );

        }

        private void changeWeight(KohenenNetwork network, Dictionary<int, KohenenNeuron> winners, List<double[]> data) {

            double s = s0 * Math.Exp(-s0 * (double)iterationNumber / T);

            foreach (KohenenNeuron[] neurons in network.getNeurons()) {
                foreach (KohenenNeuron neuron in neurons) {

                    double[] weights = neuron.getWeights();

                    double[] numerators = new double[weights.Length];
                    double[] denominators = new double[weights.Length];

                    List<KohenenNeuron> nearestNeurons = getNearestNeurons(network, neuron, s);
                    foreach (KohenenNeuron nearest in nearestNeurons) {

                        double distance = getDistance(nearest, neuron);
                        double h = Math.Exp(-distance*distance / (2 * Math.Pow(s, 2)));

                        double[] averageWeights = getAveragesValuesForWinner(nearest, winners, data);

                        for (int i = 0; i < averageWeights.Length; i++){
                            double numerator = nearest.getWinCount() * h * averageWeights[i];
                            numerators[i] += numerator;
                            double denominator = nearest.getWinCount() * h;
                            denominators[i] += denominator;
                        }
                    }

                    for (int i = 0; i < weights.Length; i++){
                        double weight = numerators[i] / denominators[i];
                        weights[i] = weight;
                    }
                }
            }
        }

        private double[] getAveragesValuesForWinner (KohenenNeuron neuron, Dictionary<int, KohenenNeuron> winners, List<double[]> data) {
            double[] averageWeights = new double[data[0].Length];

            if (neuron.getWinCount() == 0) {
                return averageWeights;
            }

            for (int i = 0; i < data.Count; i++) {
                KohenenNeuron winner = winners[i];
                if (winner == neuron) {
                    double[] input = data[i];
                    add(averageWeights, input);
                }
            }

            for (int i = 0; i < averageWeights.Length; i++){
                averageWeights[i] /= neuron.getWinCount();
            }

            return averageWeights;
        }

        private void add (double[] destination, double[] addendum) {

            for (int i = 0; i < destination.Length; i++){
                destination[i] += addendum[i];
            }
        }

        private List<KohenenNeuron> getNearestNeurons(KohenenNetwork network, KohenenNeuron current, double maxDistance) 
        {

            List<KohenenNeuron> nearest = new List<KohenenNeuron>();

            foreach (KohenenNeuron[] neurons in network.getNeurons()) {
                foreach (KohenenNeuron neuron in neurons) {

                    double distance = getDistance(current, neuron);

                    if (distance <= maxDistance) {
                        nearest.Add(neuron);
                    }
                }
            }

            return nearest;
        }

        public override void SetLogger(ILogger logger)
        {
            this.logger = logger;
        }

        private void Log(string message)
        {
            if (logger != null)
            {
                logger.Log(message);
            }
        }
    }
}
